package biolabs.model.vis.j2d;

import java.awt.BasicStroke;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import javax.vecmath.Point2d;
import javax.vecmath.Point2i;
import javax.vecmath.Tuple2d;

import biolabs.space.Continuous2DPosition;
import biolabs.space.Discrete2DPosition;
import biolabs.space.GridShape;
import biolabs.space.GridShapedObject;
import biolabs.visual.Visualizer;
import biolabs.visual.j2d.Java2DPainter;

/**
 * Visualizer that draws an outline of the object's shape.
 */
public class GridShaped2DObjectVisualizer
implements Visualizer<GridShapedObject<Discrete2DPosition, Continuous2DPosition>, Java2DPainter> {

	@Override
	public Java2DPainter visualize(GridShapedObject<Discrete2DPosition, Continuous2DPosition> o) {
		Collection<Point2i[]> paths = getOutline(o.getShape());
		return new SmoothPainter(paths);
	}

	static Collection<Point2i[]> getOutline(GridShape<Discrete2DPosition, Continuous2DPosition> shape) {
		Set<Discrete2DPosition> elems = shape.getElements();
		Set<Discrete2DPosition> border = shape.getBorderElements();
		PathBuilder pb = new PathBuilder();
		for(Discrete2DPosition p: border) {
			for(Discrete2DPosition n: p.getNeighbors()) {
				if(!elems.contains(n)) {
					Discrete2DPosition d = n.minus(p);
					Point2i a, b;
					if(d.getX() != 0) {
						int y1 = p.getY();
						int y2 = y1+1;
						int x = (d.getX()==-1) ? p.getX() : p.getX()+1;
						a = new Point2i(x, y1);
						b = new Point2i(x, y2);
					} else {
						assert d.getY() != 0;
						int x1 = p.getX();
						int x2 = x1+1;
						int y = (d.getY()==-1) ? p.getY() : p.getY()+1;
						a = new Point2i(x1, y);
						b = new Point2i(x2, y);
					}
					pb.addSegment(a, b);
				}
			}
		}
		assert pb.allPathsClosed();
		Collection<Path> paths = pb.getPaths();
		Collection<Point2i[]> optimized = new ArrayList<Point2i[]>(paths.size());
		for(Path p: paths)
			optimized.add(p.optimize());
		return optimized;
	}
}

class Path {
	private final LinkedList<Point2i> points = new LinkedList<Point2i>();

	public Path(Point2i start, Point2i end) {
		points.add(start);
		points.add(end);
	}

	public Point2i getFirst() {
		return points.getFirst();
	}

	public Point2i getLast() {
		return points.getLast();
	}

	public List<Point2i> getPoints() {
		return Collections.unmodifiableList(points);
	}

	public void addFirst(Point2i p) {
		points.addFirst(p);
	}

	public void addLast(Point2i p) {
		points.addLast(p);
	}

	public boolean isClosed() {
		return points.getFirst().equals(points.getLast());
	}

	public void join(Path p) {
		if(points.getLast().equals(p.getFirst())) {
			for(Point2i a: p.points.subList(1, p.points.size()))
				points.addLast(a);
		} else if(points.getFirst().equals(p.getLast())) {
			ListIterator<Point2i> j = points.listIterator();
			for(Point2i a: p.points.subList(0, p.points.size()-1))
				j.add(a);
		} else if(points.getLast().equals(p.getLast())) {
			ListIterator<Point2i> i = p.points.listIterator(p.points.size()-1);
			while(i.hasPrevious())
				points.addLast(i.previous());
		} else if(points.getFirst().equals(p.points.getFirst())) {
			for(Point2i a: p.points.subList(1, p.points.size()))
				points.addFirst(a);
		} else {
			throw new IllegalArgumentException("The paths cannot be joined because they don't have a common endpoint");
		}
	}

	public Point2i[] optimize() {
		int n = points.size();
		Point2i[] pts = new Point2i[n];
		points.toArray(pts);
		int prev = 0, thiz = 1, next = 2;
		while(next < n) {
			if(!inLine(pts[prev], pts[thiz], pts[next])) {
				pts[++prev] = pts[thiz];
			}
			++thiz;
			++next;
		}
		pts[++prev] = pts[n-1];
		if(pts[0].equals(pts[prev]) && prev > 2) { // the path is closed and contains more than 2 points
			if(inLine(pts[prev-1], pts[0], pts[1])) {
				--prev;
				pts[0] = pts[prev];
			}
		}

		if(prev+1 < n)
			return Arrays.copyOf(pts, prev+1);
		else
			return pts;
	}

	private static boolean inLine(Point2i a, Point2i b, Point2i c) {
		return a.x == b.x && b.x == c.x || a.y == b.y && b.y == c.y;
	}
}

class PathBuilder {
	private final Map<Point2i, Path> paths = new HashMap<Point2i, Path>();

	public void addSegment(Point2i a, Point2i b) {
		if(a.equals(b))
			throw new IllegalArgumentException("Segment's start and end are the same point");
		Path p = paths.remove(a);
		if(p != null) {
			if(a.equals(p.getFirst())) {
				paths.remove(p.getLast());
				p.addFirst(b);
			} else {
				assert a.equals(p.getLast());
				paths.remove(p.getFirst());
				p.addLast(b);
			}
			Path q = paths.remove(b);
			if(q != null) {
				p.join(q);
				paths.remove(q.getFirst());
				paths.remove(q.getLast());
			}
			paths.put(p.getFirst(), p);
			paths.put(p.getLast(), p);
		} else {
			p = paths.remove(b);
			if(p != null) {
				if(b.equals(p.getFirst())) {
					p.addFirst(a);
				} else {
					assert b.equals(p.getLast());
					p.addLast(a);
				}
				paths.put(p.getFirst(), p);
				paths.put(p.getLast(), p);
			} else {
				p = new Path(a, b);
				paths.put(a, p);
				paths.put(b, p);
			}
		}
	}

	public boolean allPathsClosed() {
		for(Path p: paths.values())
			if(!p.isClosed())
				return false;
		return true;
	}

	public Set<Path> getPaths() {
		return new HashSet<Path>(paths.values());
	}
}

abstract class GridShaped2DObjectPainter implements Java2DPainter {

	protected abstract Shape[] getShapes();

	@Override
	public void paint(Graphics2D graphics) {

		// adjust the stroke so that it is approx. 2px,
		// irrespective of scaling
		Stroke savedStroke = graphics.getStroke();
		AffineTransform tr = graphics.getTransform();
		double scale = Math.min(tr.getScaleX(), tr.getScaleY());
		BasicStroke stroke = new BasicStroke((float)(2/scale));
		graphics.setStroke(stroke);

		// draw shapes
		for(Shape s: getShapes())
			graphics.draw(s);

		// restore the original stroke
		graphics.setStroke(savedStroke);
	}

	@Override
	public Rectangle getBounds() {
		Shape[] shapes = getShapes();
		if(shapes.length == 0)
			return new Rectangle(0, 0);
		Rectangle bounds = shapes[0].getBounds();
		for(int i=1; i < shapes.length; ++i)
			bounds.add(shapes[i].getBounds());
		return bounds;
	}

}

class RoughPainter extends GridShaped2DObjectPainter {

	private final Polygon[] shapes;

	public RoughPainter(Collection<Point2i[]> paths) {
		int n = paths.size();
		shapes = new Polygon[n];
		int i = 0;
		for(Point2i[] p: paths) {
			int l = p.length - 1;
			int[] xs = new int[l];
			int[] ys = new int[l];
			for(int j = 0; j < l; ++j) {
				Point2i a = p[j];
				xs[j] = a.x;
				ys[j] = a.y;
			}
			shapes[i] = new Polygon(xs, ys, l);
			++i;
		}
	}

	@Override
	public Shape[] getShapes() {
		return shapes;
	}
}

class SmoothPainter extends GridShaped2DObjectPainter {

	private final Path2D[] shapes;

	public SmoothPainter(Collection<Point2i[]> paths) {
		shapes = new Path2D[paths.size()];

		int j = 0;
		for(Point2i[] p: paths) {
			Path2D path = new Path2D.Double();
			Point2i a, b;
			Point2d z = new Point2d();
			Point2d cp1 = new Point2d();
			Point2d cp2 = new Point2d();

			a = p[p.length-2];
			b = p[p.length-1];
			Tuple2d d = getDirection(a, b);
			z.set(b.x - d.x/2, b.y - d.y/2);
			path.moveTo(z.x, z.y);
			cp1.set(b.x - d.x/4, b.y - d.y/4);

			for(int i=0; i < p.length-1; ++i) {
				a = p[i];
				b = p[i+1];

				Tuple2d dir = getDirection(a, b);

				// calculate the target point
				z.set(a.x + dir.x/2, a.y + dir.y/2);

				// calculate the second bezier control point
				cp2.set(a.x + dir.x/4, a.y + dir.y/4);

				// draw bezier curve to the target point z
				path.curveTo(cp1.x, cp1.y, cp2.x, cp2.y, z.x, z.y);

				// calculate the next target point
				z.set(b.x - dir.x/2, b.y - dir.y/2);

				// draw line to the next target point
				path.lineTo(z.x, z.y);

				// calculate the first bezier control point
				cp1.set(b.x - dir.x/4, b.y - dir.y/4);
			}

			shapes[j] = path;
			++j;
		}
	}

	private static Tuple2d getDirection(Point2i a, Point2i b) {
		int dirx = b.x - a.x;
		int diry = b.y - a.y;

		if(dirx > 0)
			dirx = 1;
		else if(dirx < 0)
			dirx = -1;

		if(diry > 0)
			diry = 1;
		else if(diry < 0)
			diry = -1;

		return new Point2d(dirx, diry);
	}

	@Override
	protected Shape[] getShapes() {
		return shapes;
	}

}
